/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 1997-2008 Sun Microsystems, Inc.  Portions Copyrighted 2008 Richard Schilling.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 *
 * Contributor(s): Richard Schilling (coderroadie@gmail.com)
 *
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.smtp;

import java.io.*;
import java.net.*;
import java.util.*;

import javax.mail.*;
import javax.mail.event.*;
import javax.mail.internet.*;

import com.sun.mail.util.*;

/**
 * This class implements the Transport abstract class using SMTP for message
 * submission and transport.
 * <p>
 * See the <a href="package-summary.html">com.sun.mail.smtp</a> package
 * documentation for further information on the SMTP protocol provider.
 * <p>
 * This class includes many protected methods that allow a subclass to extend
 * this class and add support for non-standard SMTP commands. The
 * {@link #issueCommand} and {@link #sendCommand} methods can be used to send
 * simple SMTP commands. Other methods such as the {@link #mailFrom} and
 * {@link #data} methods can be overridden to insert new commands before or
 * after the corresponding SMTP commands. For example, a subclass could do this
 * to send the XACT command before sending the DATA command:
 * 
 * <pre>
 * 
 * protected OutputStream data() throws MessagingException {
 *     if (supportsExtension(&quot;XACCOUNTING&quot;))
 *         issueCommand(&quot;XACT&quot;, 25);
 *     return super.data();
 * }
 * </pre>
 * 
 * @author Max Spivak
 * @author Bill Shannon
 * @author Dean Gibson (DIGEST-MD5 authentication)
 * @see javax.mail.event.ConnectionEvent
 * @see javax.mail.event.TransportEvent
 */

public class SMTPTransport extends Transport {

    private String name = "smtp"; // Name of this protocol
    private int defaultPort = 25; // default SMTP port
    private boolean isSSL = false; // use SSL?
    private String host; // host we're connected to

    // Following fields valid only during the sendMessage method.
    private MimeMessage message; // Message to be sent
    private Address[] addresses; // Addresses to which to send the msg
    // Valid sent, valid unsent and invalid addresses
    private Address[] validSentAddr, validUnsentAddr, invalidAddr;
    // Did we send the message even though some addresses were invalid?
    private boolean sendPartiallyFailed = false;
    // If so, here's an exception we need to throw
    private MessagingException exception;
    // stream where message data is written
    private SMTPOutputStream dataStream;

    // Map of SMTP service extensions supported by server, if EHLO used.
    private Hashtable extMap;

    private Map authenticators = new HashMap();
    private String defaultAuthenticationMechanisms; // set in constructor

    private boolean quitWait = false; // true if we should wait
    private String saslRealm = UNKNOWN;

    private boolean reportSuccess; // throw an exception even on success
    private boolean useStartTLS; // use STARTTLS command
    private boolean requireStartTLS; // require STARTTLS command
    private boolean useRset; // use RSET instead of NOOP

    private PrintStream out; // debug output stream
    private String localHostName; // our own host name
    private String lastServerResponse; // last SMTP response
    private int lastReturnCode; // last SMTP return code
    private boolean notificationDone; // only notify once per send

    /** Headers that should not be included when sending */
    private static final String[] ignoreList = { "Bcc", "Content-Length" };
    private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };
    private static final String UNKNOWN = "UNKNOWN"; // place holder

    /**
     * Constructor that takes a Session object and a URLName that represents a
     * specific SMTP server.
     */
    public SMTPTransport(Session session, URLName urlname) {
        this(session, urlname, "smtp", false);
    }

    /**
     * Constructor used by this class and by SMTPSSLTransport subclass.
     */
    protected SMTPTransport(Session session, URLName urlname, String name, boolean isSSL) {
        super(session, urlname);
        if (urlname != null)
            name = urlname.getProtocol();
        this.name = name;
        if (!isSSL)
            isSSL = PropUtil.getBooleanSessionProperty(session, "mail." + name + ".ssl.enable",
                    false);
        if (isSSL)
            this.defaultPort = 465;
        else
            this.defaultPort = 25;
        this.isSSL = isSSL;

        out = session.getDebugOut();

        // setting mail.smtp.quitwait to false causes us to not wait for the
        // response from the QUIT command
        quitWait = PropUtil.getBooleanSessionProperty(session, "mail." + name + ".quitwait", true);

        // mail.smtp.reportsuccess causes us to throw an exception on success
        reportSuccess = PropUtil.getBooleanSessionProperty(session, "mail." + name
                + ".reportsuccess", false);

        // mail.smtp.starttls.enable enables use of STARTTLS command
        useStartTLS = PropUtil.getBooleanSessionProperty(session, "mail." + name
                + ".starttls.enable", false);

        // mail.smtp.starttls.required requires use of STARTTLS command
        requireStartTLS = PropUtil.getBooleanSessionProperty(session, "mail." + name
                + ".starttls.required", false);

        // mail.smtp.userset causes us to use RSET instead of NOOP
        // for isConnected
        useRset = PropUtil.getBooleanSessionProperty(session, "mail." + name + ".userset", false);

        // created here, because they're inner classes that reference "this"
        Authenticator[] a = new Authenticator[] { new LoginAuthenticator(), new PlainAuthenticator(), new DigestMD5Authenticator() };
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < a.length; i++) {
            authenticators.put(a[i].getMechanism(), a[i]);
            sb.append(a[i].getMechanism()).append(' ');
        }
        defaultAuthenticationMechanisms = sb.toString();
    }

    /**
     * Get the name of the local host, for use in the EHLO and HELO commands.
     * The property mail.smtp.localhost overrides mail.smtp.localaddress, which
     * overrides what InetAddress would tell us.
     */
    public synchronized String getLocalHost() {
        try {
            // get our hostname and cache it for future use
            if (localHostName == null || localHostName.length() <= 0)
                localHostName = session.getProperty("mail." + name + ".localhost");
            if (localHostName == null || localHostName.length() <= 0)
                localHostName = session.getProperty("mail." + name + ".localaddress");
            if (localHostName == null || localHostName.length() <= 0) {
                InetAddress localHost = InetAddress.getLocalHost();
                localHostName = localHost.getHostName();
                // if we can't get our name, use local address literal
                if (localHostName == null)
                    // XXX - not correct for IPv6
                    localHostName = "[" + localHost.getHostAddress() + "]";
            }
        } catch (UnknownHostException uhex) {}
        return localHostName;
    }

    /**
     * Set the name of the local host, for use in the EHLO and HELO commands.
     * 
     * @since JavaMail 1.3.1
     */
    public synchronized void setLocalHost(String localhost) {
        localHostName = localhost;
    }

    /**
     * Start the SMTP protocol on the given socket, which was already connected
     * by the caller. Useful for implementing the SMTP ATRN command (RFC 2645)
     * where an existing connection is used when the server reverses roles and
     * becomes the client.
     * 
     * @since JavaMail 1.3.3
     */
    public synchronized void connect(Socket socket) throws MessagingException {
        serverSocket = socket;
        super.connect();
    }

    /**
     * Gets the SASL realm to be used for DIGEST-MD5 authentication.
     * 
     * @return the name of the realm to use for SASL authentication.
     * @since JavaMail 1.3.1
     */
    public synchronized String getSASLRealm() {
        if (saslRealm == UNKNOWN) {
            saslRealm = session.getProperty("mail." + name + ".sasl.realm");
            if (saslRealm == null) // try old name
                saslRealm = session.getProperty("mail." + name + ".saslrealm");
        }
        return saslRealm;
    }

    /**
     * Sets the SASL realm to be used for DIGEST-MD5 authentication.
     * 
     * @param saslRealm
     *            the name of the realm to use for SASL authentication.
     * @since JavaMail 1.3.1
     */
    public synchronized void setSASLRealm(String saslRealm) {
        this.saslRealm = saslRealm;
    }

    /**
     * Should we report even successful sends by throwing an exception? If so, a
     * <code>SendFailedException</code> will always be thrown and an
     * {@link com.sun.mail.smtp.SMTPAddressSucceededException
     * SMTPAddressSucceededException} will be included in the exception chain
     * for each successful address, along with the usual
     * {@link com.sun.mail.smtp.SMTPAddressFailedException
     * SMTPAddressFailedException} for each unsuccessful address.
     * 
     * @return true if an exception will be thrown on successful sends.
     * @since JavaMail 1.3.2
     */
    public synchronized boolean getReportSuccess() {
        return reportSuccess;
    }

    /**
     * Set whether successful sends should be reported by throwing an exception.
     * 
     * @param reportSuccess
     *            should we throw an exception on success?
     * @since JavaMail 1.3.2
     */
    public synchronized void setReportSuccess(boolean reportSuccess) {
        this.reportSuccess = reportSuccess;
    }

    /**
     * Should we use the STARTTLS command to secure the connection if the server
     * supports it?
     * 
     * @return true if the STARTTLS command will be used
     * @since JavaMail 1.3.2
     */
    public synchronized boolean getStartTLS() {
        return useStartTLS;
    }

    /**
     * Set whether the STARTTLS command should be used.
     * 
     * @param useStartTLS
     *            should we use the STARTTLS command?
     * @since JavaMail 1.3.2
     */
    public synchronized void setStartTLS(boolean useStartTLS) {
        this.useStartTLS = useStartTLS;
    }

    /**
     * Should we require the STARTTLS command to secure the connection?
     * 
     * @return true if the STARTTLS command will be required
     * @since JavaMail 1.4.2
     */
    public synchronized boolean getRequireStartTLS() {
        return requireStartTLS;
    }

    /**
     * Set whether the STARTTLS command should be required.
     * 
     * @param requireStartTLS
     *            should we require the STARTTLS command?
     * @since JavaMail 1.4.2
     */
    public synchronized void setRequireStartTLS(boolean requireStartTLS) {
        this.requireStartTLS = requireStartTLS;
    }

    /**
     * Should we use the RSET command instead of the NOOP command in the @{link
     * #isConnected isConnected} method?
     * 
     * @return true if RSET will be used
     * @since JavaMail 1.4
     */
    public synchronized boolean getUseRset() {
        return useRset;
    }

    /**
     * Set whether the RSET command should be used instead of the NOOP command
     * in the @{link #isConnected isConnected} method.
     * 
     * @param useRset
     *            should we use the RSET command?
     * @since JavaMail 1.4
     */
    public synchronized void setUseRset(boolean useRset) {
        this.useRset = useRset;
    }

    /**
     * Return the last response we got from the server. A failed send is often
     * followed by an RSET command, but the response from the RSET command is
     * not saved. Instead, this returns the response from the command before the
     * RSET command.
     * 
     * @return last response from server
     * @since JavaMail 1.3.2
     */
    public synchronized String getLastServerResponse() {
        return lastServerResponse;
    }

    /**
     * Return the return code from the last response we got from the server.
     * 
     * @return return code from last response from server
     * @since JavaMail 1.4.1
     */
    public synchronized int getLastReturnCode() {
        return lastReturnCode;
    }

    /**
     * Performs the actual protocol-specific connection attempt. Will attempt to
     * connect to "localhost" if the host was null.
     * <p>
     * Unless mail.smtp.ehlo is set to false, we'll try to identify ourselves
     * using the ESMTP command EHLO. If mail.smtp.auth is set to true, we insist
     * on having a username and password, and will try to authenticate ourselves
     * if the server supports the AUTH extension (RFC 2554).
     * 
     * @param host
     *            the name of the host to connect to
     * @param port
     *            the port to use (-1 means use default port)
     * @param user
     *            the name of the user to login as
     * @param passwd
     *            the user's password
     * @return true if connection successful, false if authentication failed
     * @exception MessagingException
     *                for non-authentication failures
     */
    protected boolean protocolConnect(String host, int port, String user, String passwd)
            throws MessagingException {
        // setting mail.smtp.ehlo to false disables attempts to use EHLO
        boolean useEhlo = PropUtil.getBooleanSessionProperty(session, "mail." + name + ".ehlo",
                true);
        // setting mail.smtp.auth to true enables attempts to use AUTH
        boolean useAuth = PropUtil.getBooleanSessionProperty(session, "mail." + name + ".auth",
                false);
        // setting mail.smtp.auth.mechanisms controls which mechanisms will
        // be used, and in what order they'll be considered. only the first
        // match is used.
        String mechs = session.getProperty("mail." + name + ".auth.mechanisms");
        if (mechs == null)
            mechs = defaultAuthenticationMechanisms;

        if (debug)
            out.println("DEBUG SMTP: useEhlo " + useEhlo + ", useAuth " + useAuth);

        /*
         * If mail.smtp.auth is set, make sure we have a valid username and
         * password, even if we might not end up using it (e.g., because the
         * server doesn't support ESMTP or doesn't support the AUTH extension).
         */
        if (useAuth && (user == null || passwd == null))
            return false;

        /*
         * If port is not specified, set it to value of mail.smtp.port property
         * if it exists, otherwise default to 25.
         */
        if (port == -1)
            port = PropUtil.getIntSessionProperty(session, "mail." + name + ".port", -1);
        if (port == -1)
            port = defaultPort;

        if (host == null || host.length() == 0)
            host = "localhost";

        boolean succeed = false;

        if (serverSocket != null)
            openServer(); // only happens from connect(socket)
        else
            openServer(host, port);

        if (useEhlo)
            succeed = ehlo(getLocalHost());
        if (!succeed)
            helo(getLocalHost());

        if (useStartTLS || requireStartTLS) {
            if (supportsExtension("STARTTLS")) {
                startTLS();
                /*
                 * Have to issue another EHLO to update list of extensions
                 * supported, especially authentication mechanisms. Don't know
                 * if this could ever fail, but we ignore failure.
                 */
                ehlo(getLocalHost());
            } else if (requireStartTLS) {
                if (debug)
                    out.println("DEBUG SMTP: STARTTLS required but not supported");
                return false;
            }
        }

        if ((useAuth || (user != null && passwd != null))
                && (supportsExtension("AUTH") || supportsExtension("AUTH=LOGIN"))) {
            if (debug) {
                out.println("DEBUG SMTP: Attempt to authenticate");
                out.println("DEBUG SMTP: check mechanisms: " + mechs);
            }

            /*
             * Loop through the list of mechanisms supplied by the user (or
             * defaulted) and try each in turn. If the server supports the
             * mechanism and we have an authenticator for the mechanism, use it.
             */
            StringTokenizer st = new StringTokenizer(mechs);
            while (st.hasMoreTokens()) {
                String m = st.nextToken().toUpperCase(Locale.ENGLISH);
                if (!supportsAuthentication(m)) {
                    if (debug)
                        out.println("DEBUG SMTP: mechanism " + m + " not supported by server");
                    continue;
                }
                Authenticator a = (Authenticator) authenticators.get(m);
                if (a == null) {
                    if (debug)
                        out.println("DEBUG SMTP: " + "no authenticator for mechanism " + m);
                    continue;
                }
                // only first supported mechanism is used
                return a.authenticate(host, user, passwd);
            }
            // if authentication fails, close connection and return false
        }

        // we connected correctly
        return true;
    }

    /**
     * Abstract base class for SMTP authentication mechanism implementations.
     */
    private abstract class Authenticator {

        protected int resp; // the response code, used by subclasses
        private String mech; // the mechanism name, set in the constructor

        Authenticator(String mech) {
            this.mech = mech.toUpperCase(Locale.ENGLISH);
        }

        String getMechanism() {
            return mech;
        }

        /**
         * Start the authentication handshake by issuing the AUTH command.
         * Delegate to the doAuth method to do the mechanism-specific part of
         * the handshake.
         */
        boolean authenticate(String host, String user, String passwd) throws MessagingException {
            // XXX - could use "initial response" capability
            resp = simpleCommand("AUTH " + mech);

            /*
             * A 530 response indicates that the server wants us to issue a
             * STARTTLS command first. Do that and try again.
             */
            if (resp == 530) {
                startTLS();
                resp = simpleCommand("AUTH " + mech);
            }
            try {
                if (resp == 334)
                    doAuth(host, user, passwd);
            } catch (IOException ex) { // should never happen, ignore
                if (debug)
                    out.println("DEBUG: SMTP: " + mech + " failed: " + ex);
            } finally {
                if (resp != 235) {
                    closeConnection();
                    return false;
                }
            }
            return true;
        }

        abstract void doAuth(String host, String user, String passwd) throws MessagingException,
                IOException;
    }

    /**
     * Perform the authentication handshake for LOGIN authentication.
     */
    private class LoginAuthenticator extends Authenticator {

        LoginAuthenticator() {
            super("LOGIN");
        }

        void doAuth(String host, String user, String passwd) throws MessagingException, IOException {
            // send username
            resp = simpleCommand(BASE64EncoderStream.encode(ASCIIUtility.getBytes(user)));
            if (resp == 334) {
                // send passwd
                resp = simpleCommand(BASE64EncoderStream.encode(ASCIIUtility.getBytes(passwd)));
            }
        }
    }

    /**
     * Perform the authentication handshake for PLAIN authentication.
     */
    private class PlainAuthenticator extends Authenticator {

        PlainAuthenticator() {
            super("PLAIN");
        }

        void doAuth(String host, String user, String passwd) throws MessagingException, IOException {
            // send "<NUL>user<NUL>passwd"
            // XXX - we don't send an authorization identity
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE);
            b64os.write(0);
            b64os.write(ASCIIUtility.getBytes(user));
            b64os.write(0);
            b64os.write(ASCIIUtility.getBytes(passwd));
            b64os.flush(); // complete the encoding

            // send username
            resp = simpleCommand(bos.toByteArray());
        }
    }

    /**
     * Perform the authentication handshake for DIGEST-MD5 authentication.
     */
    private class DigestMD5Authenticator extends Authenticator {

        private DigestMD5 md5support; // only create if needed

        DigestMD5Authenticator() {
            super("DIGEST-MD5");
        }

        private synchronized DigestMD5 getMD5() {
            if (md5support == null)
                md5support = new DigestMD5(debug ? out : null);
            return md5support;
        }

        void doAuth(String host, String user, String passwd) throws MessagingException, IOException {
            DigestMD5 md5 = getMD5();
            if (md5 == null) {
                resp = -1;
                return; // XXX - should never happen
            }

            byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), getLastServerResponse());
            resp = simpleCommand(b);
            if (resp == 334) { // client authenticated by server
                if (!md5.authServer(getLastServerResponse())) {
                    // server NOT authenticated by client !!!
                    resp = -1;
                } else {
                    // send null response
                    resp = simpleCommand(new byte[0]);
                }
            }
        }
    }

    /**
     * Send the Message to the specified list of addresses.
     * <p>
     * If all the <code>addresses</code> succeed the SMTP check using the
     * <code>RCPT TO:</code> command, we attempt to send the message. A
     * TransportEvent of type MESSAGE_DELIVERED is fired indicating the
     * successful submission of a message to the SMTP host.
     * <p>
     * If some of the <code>addresses</code> fail the SMTP check, and the
     * <code>mail.stmp.sendpartial</code> property is not set, sending is
     * aborted. The TransportEvent of type MESSAGE_NOT_DELIVERED is fired
     * containing the valid and invalid addresses. The SendFailedException is
     * also thrown.
     * <p>
     * If some of the <code>addresses</code> fail the SMTP check, and the
     * <code>mail.stmp.sendpartial</code> property is set to true, the message
     * is sent. The TransportEvent of type MESSAGE_PARTIALLY_DELIVERED is fired
     * containing the valid and invalid addresses. The SMTPSendFailedException
     * is also thrown.
     * <p>
     * MessagingException is thrown if the message can't write out an
     * RFC822-compliant stream using its <code>writeTo</code> method.
     * <p>
     * 
     * @param message
     *            The MimeMessage to be sent
     * @param addresses
     *            List of addresses to send this message to
     * @see javax.mail.event.TransportEvent
     * @exception SMTPSendFailedException
     *                if the send failed because of an SMTP command error
     * @exception SendFailedException
     *                if the send failed because of invalid addresses.
     * @exception MessagingException
     *                if the connection is dead or not in the connected state or
     *                if the message is not a MimeMessage.
     */
    public synchronized void sendMessage(Message message, Address[] addresses)
            throws MessagingException, SendFailedException {

        checkConnected();

        // check if the message is a valid MIME/RFC822 message and that
        // it has all valid InternetAddresses; fail if not
        if (!(message instanceof MimeMessage)) {
            if (debug)
                out.println("DEBUG SMTP: Can only send RFC822 msgs");
            throw new MessagingException("SMTP can only send RFC822 messages");
        }
        for (int i = 0; i < addresses.length; i++) {
            if (!(addresses[i] instanceof InternetAddress)) {
                throw new MessagingException(addresses[i] + " is not an InternetAddress");
            }
        }

        this.message = (MimeMessage) message;
        this.addresses = addresses;
        validUnsentAddr = addresses; // until we know better
        expandGroups();

        boolean use8bit = false;
        if (message instanceof SMTPMessage)
            use8bit = ((SMTPMessage) message).getAllow8bitMIME();
        if (!use8bit)
            use8bit = PropUtil.getBooleanSessionProperty(session,
                    "mail." + name + ".allow8bitmime", false);
        if (debug)
            out.println("DEBUG SMTP: use8bit " + use8bit);
        if (use8bit && supportsExtension("8BITMIME")) {
            if (convertTo8Bit(this.message)) {
                // in case we made any changes, save those changes
                // XXX - this will change the Message-ID
                try {
                    this.message.saveChanges();
                } catch (MessagingException mex) {
                    // ignore it
                }
            }
        }

        try {
            mailFrom();
            rcptTo();
            this.message.writeTo(data(), ignoreList);
            finishData();
            if (sendPartiallyFailed) {
                // throw the exception,
                // fire TransportEvent.MESSAGE_PARTIALLY_DELIVERED event
                if (debug)
                    out.println("DEBUG SMTP: Sending partially failed "
                            + "because of invalid destination addresses");
                notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, validSentAddr,
                        validUnsentAddr, invalidAddr, this.message);

                throw new SMTPSendFailedException(".", lastReturnCode, lastServerResponse,
                        exception, validSentAddr, validUnsentAddr, invalidAddr);
            }
            notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, validSentAddr,
                    validUnsentAddr, invalidAddr, this.message);
        } catch (MessagingException mex) {
            if (debug)
                mex.printStackTrace(out);
            addressesFailed();
            notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr,
                    validUnsentAddr, invalidAddr, this.message);

            throw mex;
        } catch (IOException ex) {
            if (debug)
                ex.printStackTrace(out);
            // if we catch an IOException, it means that we want
            // to drop the connection so that the message isn't sent
            try {
                closeConnection();
            } catch (MessagingException mex) { /* ignore it */
            }
            addressesFailed();
            notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr,
                    validUnsentAddr, invalidAddr, this.message);

            throw new MessagingException("IOException while sending message", ex);
        } finally {
            // no reason to keep this data around
            validSentAddr = validUnsentAddr = invalidAddr = null;
            this.addresses = null;
            this.message = null;
            this.exception = null;
            sendPartiallyFailed = false;
            notificationDone = false; // reset for next send
        }
    }

    /**
     * The send failed, fix the address arrays to report the failure correctly.
     */
    private void addressesFailed() {
        if (validSentAddr != null) {
            if (validUnsentAddr != null) {
                Address newa[] = new Address[validSentAddr.length + validUnsentAddr.length];
                System.arraycopy(validSentAddr, 0, newa, 0, validSentAddr.length);
                System.arraycopy(validUnsentAddr, 0, newa, validSentAddr.length,
                        validUnsentAddr.length);
                validSentAddr = null;
                validUnsentAddr = newa;
            } else {
                validUnsentAddr = validSentAddr;
                validSentAddr = null;
            }
        }
    }

    /**
     * Close the Transport and terminate the connection to the server.
     */
    public synchronized void close() throws MessagingException {
        if (!super.isConnected()) // Already closed.
            return;
        try {
            if (serverSocket != null) {
                sendCommand("QUIT");
                if (quitWait) {
                    int resp = readServerResponse();
                    if (resp != 221 && resp != -1 && debug)
                        out.println("DEBUG SMTP: QUIT failed with " + resp);
                }
            }
        } finally {
            closeConnection();
        }
    }

    private void closeConnection() throws MessagingException {
        try {
            if (serverSocket != null)
                serverSocket.close();
        } catch (IOException ioex) { // shouldn't happen
            throw new MessagingException("Server Close Failed", ioex);
        } finally {
            serverSocket = null;
            serverOutput = null;
            serverInput = null;
            lineInputStream = null;
            if (super.isConnected()) // only notify if already connected
                super.close();
        }
    }

    /**
     * Check whether the transport is connected. Override superclass method, to
     * actually ping our server connection.
     */
    public synchronized boolean isConnected() {
        if (!super.isConnected())
            // if we haven't been connected at all, don't bother with NOOP
            return false;

        try {
            // sendmail may respond slowly to NOOP after many requests
            // so if mail.smtp.userset is set we use RSET instead of NOOP.
            if (useRset)
                sendCommand("RSET");
            else
                sendCommand("NOOP");
            int resp = readServerResponse();

            // NOOP should return 250 on success, however, SIMS 3.2 returns
            // 200, so we work around it.
            //
            // Hotmail doesn't implement the NOOP command at all so assume
            // any kind of response means we're still connected.
            // That is, any response except 421, which means the server
            // is shutting down the connection.
            //
            if (resp >= 0 && resp != 421) {
                return true;
            } else {
                try {
                    closeConnection();
                } catch (MessagingException mex) {} // ignore it
                return false;
            }
        } catch (Exception ex) {
            try {
                closeConnection();
            } catch (MessagingException mex) {} // ignore it
            return false;
        }
    }

    /**
     * Notify all TransportListeners. Keep track of whether notification has
     * been done so as to only notify once per send.
     * 
     * @since JavaMail 1.4.2
     */
    protected void notifyTransportListeners(int type, Address[] validSent, Address[] validUnsent,
            Address[] invalid, Message msg) {

        if (!notificationDone) {
            super.notifyTransportListeners(type, validSent, validUnsent, invalid, msg);
            notificationDone = true;
        }
    }

    /**
     * Expand any group addresses.
     */
    private void expandGroups() {
        Vector groups = null;
        for (int i = 0; i < addresses.length; i++) {
            InternetAddress a = (InternetAddress) addresses[i];
            if (a.isGroup()) {
                if (groups == null) {
                    // first group, catch up with where we are
                    groups = new Vector();
                    for (int k = 0; k < i; k++)
                        groups.addElement(addresses[k]);
                }
                // parse it and add each individual address
                try {
                    InternetAddress[] ia = a.getGroup(true);
                    if (ia != null) {
                        for (int j = 0; j < ia.length; j++)
                            groups.addElement(ia[j]);
                    } else
                        groups.addElement(a);
                } catch (ParseException pex) {
                    // parse failed, add the whole thing
                    groups.addElement(a);
                }
            } else {
                // if we've started accumulating a list, add this to it
                if (groups != null)
                    groups.addElement(a);
            }
        }

        // if we have a new list, convert it back to an array
        if (groups != null) {
            InternetAddress[] newa = new InternetAddress[groups.size()];
            groups.copyInto(newa);
            addresses = newa;
        }
    }

    /**
     * If the Part is a text part and has a Content-Transfer-Encoding of
     * "quoted-printable" or "base64", and it obeys the rules for "8bit"
     * encoding, change the encoding to "8bit". If the part is a multipart,
     * recursively process all its parts.
     * 
     * @return true if any changes were made
     *         XXX - This is really quite a hack.
     */
    private boolean convertTo8Bit(MimePart part) {
        boolean changed = false;
        try {
            if (part.isMimeType("text/*")) {
                String enc = part.getEncoding();
                if (enc != null
                        && (enc.equalsIgnoreCase("quoted-printable") || enc
                                .equalsIgnoreCase("base64"))) {
                    InputStream is = null;
                    try {
                        is = part.getInputStream();
                        if (is8Bit(is)) {
                            /*
                             * If the message was created using an InputStream
                             * then we have to extract the content as an object
                             * and set it back as an object so that the content
                             * will be re-encoded.
                             * 
                             * If the message was not created using an
                             * InputStream, the following should have no effect.
                             */
                            part.setContent(part.getContent(), part.getContentType());
                            part.setHeader("Content-Transfer-Encoding", "8bit");
                            changed = true;
                        }
                    } finally {
                        if (is != null) {
                            try {
                                is.close();
                            } catch (IOException ex2) {
                                // ignore it
                            }
                        }
                    }
                }
            } else if (part.isMimeType("multipart/*")) {
                MimeMultipart mp = (MimeMultipart) part.getContent();
                int count = mp.getCount();
                for (int i = 0; i < count; i++) {
                    if (convertTo8Bit((MimePart) mp.getBodyPart(i)))
                        changed = true;
                }
            }
        } catch (IOException ioex) {
            // any exception causes us to give up
        } catch (MessagingException mex) {
            // any exception causes us to give up
        }
        return changed;
    }

    /**
     * Check whether the data in the given InputStream follows the rules for
     * 8bit text. Lines have to be 998 characters or less and no NULs are
     * allowed. CR and LF must occur in pairs but we don't check that because we
     * assume this is text and we convert all CR/LF combinations into canonical
     * CRLF later.
     */
    private boolean is8Bit(InputStream is) {
        int b;
        int linelen = 0;
        boolean need8bit = false;
        try {
            while ((b = is.read()) >= 0) {
                b &= 0xff;
                if (b == '\r' || b == '\n')
                    linelen = 0;
                else if (b == 0)
                    return false;
                else {
                    linelen++;
                    if (linelen > 998) // 1000 - CRLF
                        return false;
                }
                if (b > 0x7f)
                    need8bit = true;
            }
        } catch (IOException ex) {
            return false;
        }
        if (debug && need8bit)
            out.println("DEBUG SMTP: found an 8bit part");
        return need8bit;
    }

    protected void finalize() throws Throwable {
        super.finalize();
        try {
            closeConnection();
        } catch (MessagingException mex) {} // ignore it
    }

    // /////////////////// smtp stuff ///////////////////////
    private BufferedInputStream serverInput;
    private LineInputStream lineInputStream;
    private OutputStream serverOutput;
    private Socket serverSocket;

    // ///// smtp protocol //////

    /**
     * Issue the <code>HELO</code> command.
     * 
     * @param domain
     *            our domain
     * @since JavaMail 1.4.1
     */
    protected void helo(String domain) throws MessagingException {
        if (domain != null)
            issueCommand("HELO " + domain, 250);
        else
            issueCommand("HELO", 250);
    }

    /**
     * Issue the <code>EHLO</code> command. Collect the returned list of service
     * extensions.
     * 
     * @param domain
     *            our domain
     * @return true if command succeeds
     * @since JavaMail 1.4.1
     */
    protected boolean ehlo(String domain) throws MessagingException {
        String cmd;
        if (domain != null)
            cmd = "EHLO " + domain;
        else
            cmd = "EHLO";
        sendCommand(cmd);
        int resp = readServerResponse();
        if (resp == 250) {
            // extract the supported service extensions
            BufferedReader rd = new BufferedReader(new StringReader(lastServerResponse));
            String line;
            extMap = new Hashtable();
            try {
                boolean first = true;
                while ((line = rd.readLine()) != null) {
                    if (first) { // skip first line which is the greeting
                        first = false;
                        continue;
                    }
                    if (line.length() < 5)
                        continue; // shouldn't happen
                    line = line.substring(4); // skip response code
                    int i = line.indexOf(' ');
                    String arg = "";
                    if (i > 0) {
                        arg = line.substring(i + 1);
                        line = line.substring(0, i);
                    }
                    if (debug)
                        out.println("DEBUG SMTP: Found extension \"" + line + "\", arg \"" + arg
                                + "\"");
                    extMap.put(line.toUpperCase(Locale.ENGLISH), arg);
                }
            } catch (IOException ex) {} // can't happen
        }
        return resp == 250;
    }

    /**
     * Issue the <code>MAIL FROM:</code> command to start sending a message.
     * <p>
     * Gets the sender's address in the following order:
     * <ol>
     * <li>SMTPMessage.getEnvelopeFrom()</li>
     * <li>mail.smtp.from property</li>
     * <li>From: header in the message</li>
     * <li>System username using the InternetAddress.getLocalAddress() method</li>
     * </ol>
     * 
     * @since JavaMail 1.4.1
     */
    protected void mailFrom() throws MessagingException {
        String from = null;
        if (message instanceof SMTPMessage)
            from = ((SMTPMessage) message).getEnvelopeFrom();
        if (from == null || from.length() <= 0)
            from = session.getProperty("mail." + name + ".from");
        if (from == null || from.length() <= 0) {
            Address[] fa;
            Address me;
            if (message != null && (fa = message.getFrom()) != null && fa.length > 0)
                me = fa[0];
            else
                me = InternetAddress.getLocalAddress(session);

            if (me != null)
                from = ((InternetAddress) me).getAddress();
            else
                throw new MessagingException("can't determine local email address");
        }

        String cmd = "MAIL FROM:" + normalizeAddress(from);

        // request delivery status notification?
        if (supportsExtension("DSN")) {
            String ret = null;
            if (message instanceof SMTPMessage)
                ret = ((SMTPMessage) message).getDSNRet();
            if (ret == null)
                ret = session.getProperty("mail." + name + ".dsn.ret");
            // XXX - check for legal syntax?
            if (ret != null)
                cmd += " RET=" + ret;
        }

        /*
         * If an RFC 2554 submitter has been specified, and the server supports
         * the AUTH extension, include the AUTH= element on the MAIL FROM
         * command.
         */
        if (supportsExtension("AUTH")) {
            String submitter = null;
            if (message instanceof SMTPMessage)
                submitter = ((SMTPMessage) message).getSubmitter();
            if (submitter == null)
                submitter = session.getProperty("mail." + name + ".submitter");
            // XXX - check for legal syntax?
            if (submitter != null) {
                try {
                    String s = xtext(submitter);
                    cmd += " AUTH=" + s;
                } catch (IllegalArgumentException ex) {
                    if (debug)
                        out.println("DEBUG SMTP: ignoring invalid submitter: " + submitter
                                + ", Exception: " + ex);
                }
            }
        }

        /*
         * Have any extensions to the MAIL command been specified?
         */
        String ext = null;
        if (message instanceof SMTPMessage)
            ext = ((SMTPMessage) message).getMailExtension();
        if (ext == null)
            ext = session.getProperty("mail." + name + ".mailextension");
        if (ext != null && ext.length() > 0)
            cmd += " " + ext;

        issueSendCommand(cmd, 250);
    }

    /**
     * Sends each address to the SMTP host using the <code>RCPT TO:</code>
     * command and copies the address either into the validSentAddr or
     * invalidAddr arrays. Sets the <code>sendFailed</code> flag to true if any
     * addresses failed.
     * 
     * @since JavaMail 1.4.1
     */
    /*
     * success/failure/error possibilities from the RCPT command from rfc821,
     * section 4.3 S: 250, 251 F: 550, 551, 552, 553, 450, 451, 452 E: 500, 501,
     * 503, 421
     * 
     * and how we map the above error/failure conditions to valid/invalid
     * address vectors that are reported in the thrown exception: invalid addr:
     * 550, 501, 503, 551, 553 valid addr: 552 (quota), 450, 451, 452 (quota),
     * 421 (srvr abort)
     */
    protected void rcptTo() throws MessagingException {
        Vector valid = new Vector();
        Vector validUnsent = new Vector();
        Vector invalid = new Vector();
        int retCode = -1;
        MessagingException mex = null;
        boolean sendFailed = false;
        MessagingException sfex = null;
        validSentAddr = validUnsentAddr = invalidAddr = null;
        boolean sendPartial = false;
        if (message instanceof SMTPMessage)
            sendPartial = ((SMTPMessage) message).getSendPartial();
        if (!sendPartial)
            sendPartial = PropUtil.getBooleanSessionProperty(session, "mail." + name
                    + ".sendpartial", false);
        if (debug && sendPartial)
            out.println("DEBUG SMTP: sendPartial set");

        boolean dsn = false;
        String notify = null;
        if (supportsExtension("DSN")) {
            if (message instanceof SMTPMessage)
                notify = ((SMTPMessage) message).getDSNNotify();
            if (notify == null)
                notify = session.getProperty("mail." + name + ".dsn.notify");
            // XXX - check for legal syntax?
            if (notify != null)
                dsn = true;
        }

        // try the addresses one at a time
        for (int i = 0; i < addresses.length; i++) {

            sfex = null;
            InternetAddress ia = (InternetAddress) addresses[i];
            String cmd = "RCPT TO:" + normalizeAddress(ia.getAddress());
            if (dsn)
                cmd += " NOTIFY=" + notify;
            // send the addresses to the SMTP server
            sendCommand(cmd);
            // check the server's response for address validity
            retCode = readServerResponse();
            switch (retCode) {
                case 250:
                case 251:
                    valid.addElement(ia);
                    if (!reportSuccess)
                        break;

                    // user wants exception even when successful, including
                    // details of the return code

                    // create and chain the exception
                    sfex = new SMTPAddressSucceededException(ia, cmd, retCode, lastServerResponse);
                    if (mex == null)
                        mex = sfex;
                    else
                        mex.setNextException(sfex);
                    break;

                case 550:
                case 553:
                case 503:
                case 551:
                case 501:
                    // given address is invalid
                    if (!sendPartial)
                        sendFailed = true;
                    invalid.addElement(ia);
                    // create and chain the exception
                    sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse);
                    if (mex == null)
                        mex = sfex;
                    else
                        mex.setNextException(sfex);
                    break;

                case 552:
                case 450:
                case 451:
                case 452:
                    // given address is valid
                    if (!sendPartial)
                        sendFailed = true;
                    validUnsent.addElement(ia);
                    // create and chain the exception
                    sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse);
                    if (mex == null)
                        mex = sfex;
                    else
                        mex.setNextException(sfex);
                    break;

                default:
                    // handle remaining 4xy & 5xy codes
                    if (retCode >= 400 && retCode <= 499) {
                        // assume address is valid, although we don't really
                        // know
                        validUnsent.addElement(ia);
                    } else if (retCode >= 500 && retCode <= 599) {
                        // assume address is invalid, although we don't really
                        // know
                        invalid.addElement(ia);
                    } else {
                        // completely unexpected response, just give up
                        if (debug)
                            out.println("DEBUG SMTP: got response code " + retCode
                                    + ", with response: " + lastServerResponse);
                        String _lsr = lastServerResponse; // else rset will nuke
                                                          // it
                        int _lrc = lastReturnCode;
                        if (serverSocket != null) // hasn't already been closed
                            issueCommand("RSET", -1);
                        lastServerResponse = _lsr; // restore, for get
                        lastReturnCode = _lrc;
                        throw new SMTPAddressFailedException(ia, cmd, retCode, _lsr);
                    }
                    if (!sendPartial)
                        sendFailed = true;
                    // create and chain the exception
                    sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse);
                    if (mex == null)
                        mex = sfex;
                    else
                        mex.setNextException(sfex);
                    break;
            }
        }

        // if we're willing to send to a partial list, and we found no
        // valid addresses, that's complete failure
        if (sendPartial && valid.size() == 0)
            sendFailed = true;

        // copy the vectors into appropriate arrays
        if (sendFailed) {
            // copy invalid addrs
            invalidAddr = new Address[invalid.size()];
            invalid.copyInto(invalidAddr);

            // copy all valid addresses to validUnsent, since something failed
            validUnsentAddr = new Address[valid.size() + validUnsent.size()];
            int i = 0;
            for (int j = 0; j < valid.size(); j++)
                validUnsentAddr[i++] = (Address) valid.elementAt(j);
            for (int j = 0; j < validUnsent.size(); j++)
                validUnsentAddr[i++] = (Address) validUnsent.elementAt(j);
        } else if (reportSuccess || (sendPartial && (invalid.size() > 0 || validUnsent.size() > 0))) {
            // we'll go on to send the message, but after sending we'll
            // throw an exception with this exception nested
            sendPartiallyFailed = true;
            exception = mex;

            // copy invalid addrs
            invalidAddr = new Address[invalid.size()];
            invalid.copyInto(invalidAddr);

            // copy valid unsent addresses to validUnsent
            validUnsentAddr = new Address[validUnsent.size()];
            validUnsent.copyInto(validUnsentAddr);

            // copy valid addresses to validSent
            validSentAddr = new Address[valid.size()];
            valid.copyInto(validSentAddr);
        } else { // all addresses pass
            validSentAddr = addresses;
        }

        // print out the debug info
        if (debug) {
            if (validSentAddr != null && validSentAddr.length > 0) {
                out.println("DEBUG SMTP: Verified Addresses");
                for (int l = 0; l < validSentAddr.length; l++) {
                    out.println("DEBUG SMTP:   " + validSentAddr[l]);
                }
            }
            if (validUnsentAddr != null && validUnsentAddr.length > 0) {
                out.println("DEBUG SMTP: Valid Unsent Addresses");
                for (int j = 0; j < validUnsentAddr.length; j++) {
                    out.println("DEBUG SMTP:   " + validUnsentAddr[j]);
                }
            }
            if (invalidAddr != null && invalidAddr.length > 0) {
                out.println("DEBUG SMTP: Invalid Addresses");
                for (int k = 0; k < invalidAddr.length; k++) {
                    out.println("DEBUG SMTP:   " + invalidAddr[k]);
                }
            }
        }

        // throw the exception, fire TransportEvent.MESSAGE_NOT_DELIVERED event
        if (sendFailed) {
            if (debug)
                out.println("DEBUG SMTP: Sending failed "
                        + "because of invalid destination addresses");
            notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr,
                    validUnsentAddr, invalidAddr, this.message);

            // reset the connection so more sends are allowed
            String lsr = lastServerResponse; // save, for get
            int lrc = lastReturnCode;
            try {
                if (serverSocket != null)
                    issueCommand("RSET", -1);
            } catch (MessagingException ex) {
                // if can't reset, best to close the connection
                try {
                    close();
                } catch (MessagingException ex2) {
                    // thrown by close()--ignore, will close() later anyway
                    if (debug)
                        ex2.printStackTrace(out);
                }
            } finally {
                lastServerResponse = lsr; // restore
                lastReturnCode = lrc;
            }

            throw new SendFailedException("Invalid Addresses", mex, validSentAddr, validUnsentAddr,
                    invalidAddr);
        }
    }

    /**
     * Send the <code>DATA</code> command to the SMTP host and return an
     * OutputStream to which the data is to be written.
     * 
     * @since JavaMail 1.4.1
     */
    protected OutputStream data() throws MessagingException {
        assert Thread.holdsLock(this);
        issueSendCommand("DATA", 354);
        dataStream = new SMTPOutputStream(serverOutput);
        return dataStream;
    }

    /**
     * Terminate the sent data.
     * 
     * @since JavaMail 1.4.1
     */
    protected void finishData() throws IOException, MessagingException {
        assert Thread.holdsLock(this);
        dataStream.ensureAtBOL();
        issueSendCommand(".", 250);
    }

    /**
     * Issue the <code>STARTTLS</code> command and switch the socket to TLS mode
     * if it succeeds.
     * 
     * @since JavaMail 1.4.1
     */
    protected void startTLS() throws MessagingException {
        issueCommand("STARTTLS", 220);
        // it worked, now switch the socket into TLS mode
        try {
            serverSocket = SocketFetcher.startTLS(serverSocket, host, session.getProperties(),
                    "mail." + name);
            initStreams();
        } catch (IOException ioex) {
            closeConnection();
            throw new MessagingException("Could not convert socket to TLS", ioex);
        }
    }

    // ///// primitives ///////

    /**
     * Connect to host on port and start the SMTP protocol.
     */
    private void openServer(String host, int port) throws MessagingException {

        if (debug)
            out.println("DEBUG SMTP: trying to connect to host \"" + host + "\", port " + port
                    + ", isSSL " + isSSL);

        try {
            Properties props = session.getProperties();

            serverSocket = SocketFetcher.getSocket(host, port, props, "mail." + name, isSSL);

            // socket factory may've chosen a different port,
            // update it for the debug messages that follow
            port = serverSocket.getPort();
            // save host name for startTLS
            this.host = host;

            initStreams();

            int r = -1;
            if ((r = readServerResponse()) != 220) {
                serverSocket.close();
                serverSocket = null;
                serverOutput = null;
                serverInput = null;
                lineInputStream = null;
                if (debug)
                    out.println("DEBUG SMTP: could not connect to host \"" + host + "\", port: "
                            + port + ", response: " + r + "\n");
                throw new MessagingException("Could not connect to SMTP host: " + host + ", port: "
                        + port + ", response: " + r);
            } else {
                if (debug)
                    out.println("DEBUG SMTP: connected to host \"" + host + "\", port: " + port
                            + "\n");
            }
        } catch (UnknownHostException uhex) {
            throw new MessagingException("Unknown SMTP host: " + host, uhex);
        } catch (IOException ioe) {
            throw new MessagingException("Could not connect to SMTP host: " + host + ", port: "
                    + port, ioe);
        }
    }

    /**
     * Start the protocol to the server on serverSocket, assumed to be provided
     * and connected by the caller.
     */
    private void openServer() throws MessagingException {
        int port = -1;
        host = "UNKNOWN";
        try {
            port = serverSocket.getPort();
            host = serverSocket.getInetAddress().getHostName();
            if (debug)
                out.println("DEBUG SMTP: starting protocol to host \"" + host + "\", port " + port);

            initStreams();

            int r = -1;
            if ((r = readServerResponse()) != 220) {
                serverSocket.close();
                serverSocket = null;
                serverOutput = null;
                serverInput = null;
                lineInputStream = null;
                if (debug)
                    out.println("DEBUG SMTP: got bad greeting from host \"" + host + "\", port: "
                            + port + ", response: " + r + "\n");
                throw new MessagingException("Got bad greeting from SMTP host: " + host
                        + ", port: " + port + ", response: " + r);
            } else {
                if (debug)
                    out.println("DEBUG SMTP: protocol started to host \"" + host + "\", port: "
                            + port + "\n");
            }
        } catch (IOException ioe) {
            throw new MessagingException("Could not start protocol to SMTP host: " + host
                    + ", port: " + port, ioe);
        }
    }

    private void initStreams() throws IOException {
        PrintStream out = session.getDebugOut();
        boolean debug = session.getDebug();
        boolean quote = PropUtil.getBooleanSessionProperty(session, "mail.debug.quote", false);

        TraceInputStream traceInput = new TraceInputStream(serverSocket.getInputStream(), out);
        traceInput.setTrace(debug);
        traceInput.setQuote(quote);

        TraceOutputStream traceOutput = new TraceOutputStream(serverSocket.getOutputStream(), out);
        traceOutput.setTrace(debug);
        traceOutput.setQuote(quote);

        serverOutput = new BufferedOutputStream(traceOutput);
        serverInput = new BufferedInputStream(traceInput);
        lineInputStream = new LineInputStream(serverInput);
    }

    /**
     * Send the command to the server. If the expected response code is not
     * received, throw a MessagingException.
     * 
     * @param cmd
     *            the command to send
     * @param expect
     *            the expected response code (-1 means don't care)
     * @since JavaMail 1.4.1
     */
    public synchronized void issueCommand(String cmd, int expect) throws MessagingException {
        sendCommand(cmd);

        // if server responded with an unexpected return code,
        // throw the exception, notifying the client of the response
        int resp = readServerResponse();
        if (expect != -1 && resp != expect)
            throw new MessagingException(lastServerResponse);
    }

    /**
     * Issue a command that's part of sending a message.
     */
    private void issueSendCommand(String cmd, int expect) throws MessagingException {
        sendCommand(cmd);

        // if server responded with an unexpected return code,
        // throw the exception, notifying the client of the response
        int ret;
        if ((ret = readServerResponse()) != expect) {
            // assume message was not sent to anyone,
            // combine valid sent & unsent addresses
            int vsl = validSentAddr == null ? 0 : validSentAddr.length;
            int vul = validUnsentAddr == null ? 0 : validUnsentAddr.length;
            Address[] valid = new Address[vsl + vul];
            if (vsl > 0)
                System.arraycopy(validSentAddr, 0, valid, 0, vsl);
            if (vul > 0)
                System.arraycopy(validUnsentAddr, 0, valid, vsl, vul);
            validSentAddr = null;
            validUnsentAddr = valid;
            if (debug)
                out.println("DEBUG SMTP: got response code " + ret + ", with response: "
                        + lastServerResponse);
            String _lsr = lastServerResponse; // else rset will nuke it
            int _lrc = lastReturnCode;
            if (serverSocket != null) // hasn't already been closed
                issueCommand("RSET", -1);
            lastServerResponse = _lsr; // restore, for get
            lastReturnCode = _lrc;
            throw new SMTPSendFailedException(cmd, ret, lastServerResponse, exception,
                    validSentAddr, validUnsentAddr, invalidAddr);
        }
    }

    /**
     * Send the command to the server and return the response code from the
     * server.
     * 
     * @since JavaMail 1.4.1
     */
    public synchronized int simpleCommand(String cmd) throws MessagingException {
        sendCommand(cmd);
        return readServerResponse();
    }

    /**
     * Send the command to the server and return the response code from the
     * server.
     * 
     * @since JavaMail 1.4.1
     */
    protected int simpleCommand(byte[] cmd) throws MessagingException {
        assert Thread.holdsLock(this);
        sendCommand(cmd);
        return readServerResponse();
    }

    /**
     * Sends command <code>cmd</code> to the server terminating it with
     * <code>CRLF</code>.
     * 
     * @since JavaMail 1.4.1
     */
    protected void sendCommand(String cmd) throws MessagingException {
        sendCommand(ASCIIUtility.getBytes(cmd));
    }

    private void sendCommand(byte[] cmdBytes) throws MessagingException {
        assert Thread.holdsLock(this);
        if (debug)
            out.println("DEBUG SMTP SENT: " + new String(cmdBytes, 0));

        try {
            serverOutput.write(cmdBytes);
            serverOutput.write(CRLF);
            serverOutput.flush();
        } catch (IOException ex) {
            throw new MessagingException("Can't send command to SMTP host", ex);
        }
    }

    /**
     * Reads server reponse returning the <code>returnCode</code> as the number.
     * Returns -1 on failure. Sets <code>lastServerResponse</code> and
     * <code>lastReturnCode</code>.
     * 
     * @return server response code
     * @since JavaMail 1.4.1
     */
    protected int readServerResponse() throws MessagingException {
        assert Thread.holdsLock(this);
        String serverResponse = "";
        int returnCode = 0;
        StringBuffer buf = new StringBuffer(100);

        // read the server response line(s) and add them to the buffer
        // that stores the response
        try {
            String line = null;

            do {
                line = lineInputStream.readLine();
                if (line == null) {
                    serverResponse = buf.toString();
                    if (serverResponse.length() == 0)
                        serverResponse = "[EOF]";
                    lastServerResponse = serverResponse;
                    lastReturnCode = -1;
                    if (debug)
                        out.println("DEBUG SMTP: EOF: " + serverResponse);
                    return -1;
                }
                buf.append(line);
                buf.append("\n");
            } while (isNotLastLine(line));

            serverResponse = buf.toString();
        } catch (IOException ioex) {
            if (debug) {
                out.println("DEBUG SMTP: exception reading response: " + ioex);
                ioex.printStackTrace(out);
            }
            lastServerResponse = "";
            lastReturnCode = 0;
            throw new MessagingException("Exception reading response", ioex);
            // returnCode = -1;
        }

        // print debug info
        if (debug)
            out.println("DEBUG SMTP RCVD: " + serverResponse);

        // parse out the return code
        if (serverResponse != null && serverResponse.length() >= 3) {
            try {
                returnCode = Integer.parseInt(serverResponse.substring(0, 3));
            } catch (NumberFormatException nfe) {
                try {
                    close();
                } catch (MessagingException mex) {
                    // thrown by close()--ignore, will close() later anyway
                    if (debug)
                        mex.printStackTrace(out);
                }
                returnCode = -1;
            } catch (StringIndexOutOfBoundsException ex) {
                // if (debug) ex.printStackTrace(out);
                try {
                    close();
                } catch (MessagingException mex) {
                    // thrown by close()--ignore, will close() later anyway
                    if (debug)
                        mex.printStackTrace(out);
                }
                returnCode = -1;
            }
        } else {
            returnCode = -1;
        }
        if (returnCode == -1 && debug)
            out.println("DEBUG SMTP: bad server response: " + serverResponse);

        lastServerResponse = serverResponse;
        lastReturnCode = returnCode;
        return returnCode;
    }

    /**
     * Check if we're in the connected state. Don't bother checking whether the
     * server is still alive, that will be detected later.
     * 
     * @exception IllegalStateException
     *                if not connected
     * @since JavaMail 1.4.1
     */
    protected void checkConnected() {
        if (!super.isConnected())
            throw new IllegalStateException("Not connected");
    }

    // tests if the <code>line</code> is an intermediate line according to SMTP
    private boolean isNotLastLine(String line) {
        return line != null && line.length() >= 4 && line.charAt(3) == '-';
    }

    // wraps an address in "<>"'s if necessary
    private String normalizeAddress(String addr) {
        if ((!addr.startsWith("<")) && (!addr.endsWith(">")))
            return "<" + addr + ">";
        else
            return addr;
    }

    /**
     * Return true if the SMTP server supports the specified service extension.
     * Extensions are reported as results of the EHLO command when connecting to
     * the server. See <A HREF="http://www.ietf.org/rfc/rfc1869.txt">RFC
     * 1869</A> and other RFCs that define specific extensions.
     * 
     * @param ext
     *            the service extension name
     * @return true if the extension is supported
     * @since JavaMail 1.3.2
     */
    public boolean supportsExtension(String ext) {
        return extMap != null && extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null;
    }

    /**
     * Return the parameter the server provided for the specified service
     * extension, or null if the extension isn't supported.
     * 
     * @param ext
     *            the service extension name
     * @return the extension parameter
     * @since JavaMail 1.3.2
     */
    public String getExtensionParameter(String ext) {
        return extMap == null ? null : (String) extMap.get(ext.toUpperCase(Locale.ENGLISH));
    }

    /**
     * Does the server we're connected to support the specified authentication
     * mechanism? Uses the extension information returned by the server from the
     * EHLO command.
     * 
     * @param auth
     *            the authentication mechanism
     * @return true if the authentication mechanism is supported
     * @since JavaMail 1.4.1
     */
    protected boolean supportsAuthentication(String auth) {
        assert Thread.holdsLock(this);
        if (extMap == null)
            return false;
        String a = (String) extMap.get("AUTH");
        if (a == null)
            return false;
        StringTokenizer st = new StringTokenizer(a);
        while (st.hasMoreTokens()) {
            String tok = st.nextToken();
            if (tok.equalsIgnoreCase(auth))
                return true;
        }
        // hack for buggy servers that advertise capability incorrectly
        if (auth.equalsIgnoreCase("LOGIN") && supportsExtension("AUTH=LOGIN")) {
            out.println("DEBUG SMTP: use AUTH=LOGIN hack");
            return true;
        }
        return false;
    }

    private static char[] hexchar = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

    /**
     * Convert a string to RFC 1891 xtext format.
     * <p>
     * 
     * <pre>
     *     xtext = *( xchar / hexchar )
     *     xchar = any ASCII CHAR between &quot;!&quot; (33) and &quot;&tilde;&quot; (126) inclusive,
     *          except for &quot;+&quot; and &quot;=&quot;.
     * ; &quot;hexchar&quot;s are intended to encode octets that cannot appear
     * ; as ASCII characters within an esmtp-value.
     *     hexchar = ASCII &quot;+&quot; immediately followed by two upper case
     *          hexadecimal digits
     * </pre>
     * 
     * </p>
     * 
     * @since JavaMail 1.4.1
     */
    protected static String xtext(String s) {
        StringBuffer sb = null;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= 128) // not ASCII
                throw new IllegalArgumentException("Non-ASCII character in SMTP submitter: " + s);
            if (c < '!' || c > '~' || c == '+' || c == '=') {
                if (sb == null) {
                    sb = new StringBuffer(s.length() + 4);
                    sb.append(s.substring(0, i));
                }
                sb.append('+');
                sb.append(hexchar[(((int) c) & 0xf0) >> 4]);
                sb.append(hexchar[((int) c) & 0x0f]);
            } else {
                if (sb != null)
                    sb.append(c);
            }
        }
        return sb != null ? sb.toString() : s;
    }
}
